Skip to content

feat(stats): wire token-savings telemetry into search and find_related - #82

Merged
amondnet merged 3 commits into
mainfrom
amondnet/savings-wiring
Sep 4, 2026
Merged

feat(stats): wire token-savings telemetry into search and find_related#82
amondnet merged 3 commits into
mainfrom
amondnet/savings-wiring

Conversation

@amondnet

@amondnet amondnet commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

save_search_stats was defined and unit-tested but never called in any code path — nothing recorded to ~/.csp/savings.jsonl, so csp savings always reported zero. This wires it in at the CLI/MCP app boundary and applies the semble#206 snippet accounting deferred from #75.

Closes #81.

What changed

  • CspIndex.file_sizes (repo-relative path → UTF-16 char count) captured at build time in from_path, carried through from_git's re-root, and recomputed in load_from_disk when the source root is a still-present local directory (mirrors semble reading sizes off root). Empty when source files aren't available (e.g. a cached git index) → file_chars is simply 0.
  • save_search_stats takes max_snippet_lines so recorded snippet_chars reflects what the caller actually received (semble#206): None → full chunk, Some(0) → 0, Some(n) → first n lines.
  • Recording wired in at search_output / find_related_output (CLI) and search_tool / find_related_tool (MCP), each behind an injected Option<&Path> stats file. CspIndex::search stays a pure, side-effect-free library call — telemetry lives at the app boundary (issue savings telemetry is not wired into the search flow (blocks semble#206) #81 permits either).

Testability / no pollution

Every recording site takes an injected Option<&Path>. The MCP server holds an injectable stats_file field (None in tests); CLI dispatch is split into dispatch_with_stats(command, stats_file). Tests redirect telemetry to a temp file or disable it, so the developer's real ~/.csp/savings.jsonl is never touched.

Testing

cargo fmt --all && cargo clippy --all-targets --all-features -- -D warnings && cargo test --workspace — 273 lib + 22 CLI tests pass (4 network-gated ignored).

Stacking

Stacked on #75 (amondnet/max-snippet-lines) — base will retarget to main once #75 merges.

🤖 Generated with Claude Code


Summary by cubic

Record token-savings telemetry for search and find-related in the CLI and MCP so csp savings shows real data, and add max_snippet_lines to cap returned snippets.

  • New Features
    • Wired save_search_stats into CLI and MCP behind an optional stats file; defaults to ~/.csp/savings.jsonl.
    • Added CspIndex.file_sizes (repo-relative path → UTF-16 char count), captured at build time, carried through from_git, recomputed on load when the local root exists, and guarded against absolute, parent-escaping, or non-regular-file chunk paths.
    • from_path now stores an absolute root so a relative-path build reloads its source from any cwd.
    • save_search_stats now takes max_snippet_lines so snippet_chars reflects delivered lines (None = full, 0 = none, n = first n lines).
    • Results are now flat {file_path, start_line, end_line, score, content?}; the nested chunk, location, and language fields are dropped. CLI defaults to full content, MCP defaults to 10 lines.
    • Kept CspIndex::search side-effect-free; tests inject None or a temp file so the real stats file is untouched. Fixes savings telemetry is not wired into the search flow (blocks semble#206) #81.

Written for commit c6652d0. Summary will update on new commits.

@codacy-production

codacy-production Bot commented Jul 11, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 26 complexity · 10 duplication

Metric Results
Complexity 26
Duplication 10

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Architecture diagram
sequenceDiagram
    participant CLI as CLI Dispatch
    participant MCP as MCP Server
    participant Stats as save_search_stats()
    participant Index as CspIndex
    participant Disk as Source Tree on Disk
    participant File as ~/.csp/savings.jsonl

    Note over CLI,MCP: Search / find-related entry points

    CLI->>Index: search(query, options)
    Index->>Disk: read_from_path(root)
    Disk-->>Index: file_sizes (UTF-16 char counts)
    Index-->>CLI: results

    CLI->>Stats: save_search_stats(stats_file, results, CallType::Search, index.file_sizes, max_snippet_lines)
    alt max_snippet_lines is None
        Stats->>Stats: snippet_chars = utf16_len(content) (full chunk)
    else max_snippet_lines is Some(0)
        Stats->>Stats: snippet_chars = 0
    else max_snippet_lines is Some(n)
        Stats->>Stats: snippet_chars = utf16_len(first n lines)
    end
    Stats->>File: Append JSONL record
    File-->>Stats: (swallow I/O errors)

    Note over MCP,Index: MCP tools with injected stats_file

    MCP->>Index: search_tool(cache, ..., stats_file)
    Index-->>MCP: results
    alt stats_file is Some
        MCP->>Stats: save_search_stats(stats_file, results, CallType::Search, index.file_sizes, max_snippet_lines)
        Stats->>File: Append JSONL record
    end

    Note over Index,Disk: File sizes lifecycle

    alt from_path() (local directory)
        Index->>Disk: compute_file_sizes(root, chunks)
        Disk-->>Index: HashMap<path, utf16_char_count>
    else from_git() (remote URL)
        Index->>Index: clone from_path computes sizes
        Index->>Index: clone file_sizes before re-rooting
    else load_from_disk() (cached manifest)
        alt root is a present local directory
            Index->>Disk: compute_file_sizes(root, chunks)
            Disk-->>Index: HashMap<path, utf16_char_count>
        else root is git URL or missing
            Index->>Index: file_sizes = empty (file_chars = 0)
        end
    end
Loading

Re-trigger cubic

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.80000% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/csp/src/mcp.rs 86.27% 7 Missing ⚠️
crates/csp/src/indexing/index.rs 98.03% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 풀 리퀘스트는 검색 및 관련 청크 찾기 기능에 토큰 절약 텔레메트리(통계 기록) 기능을 추가하는 변경사항을 담고 있습니다. 인덱스 빌드 시 파일 크기를 캡처하고, 검색 결과 출력 시 실제 전달된 문자 수를 계산하여 통계 파일에 기록하도록 구현되었습니다. 코드 리뷰 결과, compute_file_sizes 함수에서 상위 디렉터리 참조(..)나 절대 경로를 검증하지 않아 발생할 수 있는 경로 탐색(Path Traversal) 취약점(HIGH)과, delivered_chars 함수에서 불필요한 메모리 할당으로 인한 성능 저하 우려(MEDIUM)가 지적되었습니다. 두 피드백 모두 타당하며 제시된 코드로 수정할 것을 권장합니다.

Comment thread crates/csp/src/indexing/index.rs
Comment thread crates/csp/src/stats.rs
@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Jul 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires the previously inert save_search_stats telemetry into the four user-facing call sites (search_output, find_related_output, search_tool, find_related_tool) and adds the file_sizes map to CspIndex so character counts are available at recording time. The injection pattern (Option<&Path> stats file, dispatch_with_stats) keeps the library layer side-effect-free and the tests clean.

  • stats.rs: max_snippet_lines is threaded through as a new parameter; delivered_chars() correctly caps the counted characters to match what callers actually receive, with new unit tests confirming the Some(0) and Some(n) branches.
  • indexing/index.rs: compute_file_sizes reads files from disk once per unique path and stores the UTF-16 lengths in CspIndex::file_sizes; the map is captured in from_path, carried through from_git's re-root, and recomputed in load_from_disk when the source directory is still reachable.
  • main.rs / mcp.rs / mcp_server.rs: Recording is wired at the app boundary after each successful search; the Command::Savings arm in dispatch_with_stats still hard-codes default_stats_file() rather than using the injected path, breaking the injection contract for that subcommand.

Confidence Score: 4/5

Safe to merge; the only concrete issue is that Command::Savings inside dispatch_with_stats reads from the hardcoded default path instead of the injected one, which has no production impact but would silently mislead a future test that pipes telemetry to a temp file and then inspects it via Savings.

The core telemetry wiring, the file_sizes capture across all three build paths, the delivered_chars accounting, and the test isolation strategy are all correct and well-tested. The one inconsistency — Savings ignoring the injected stats_file — is a latent gap in the injection contract, not a runtime defect in any currently exercised path.

crates/csp/src/bin/csp/main.rs (the Command::Savings arm) and crates/csp/src/bin/csp/mcp_server.rs (the find_related test that skips nullifying stats_file).

Important Files Changed

Filename Overview
crates/csp/src/stats.rs Adds max_snippet_lines parameter to save_search_stats and introduces delivered_chars() to cap counted characters per the semble#206 spec. Logic and new tests are correct.
crates/csp/src/indexing/index.rs Adds file_sizes: HashMap<String, u64> to CspIndex, computed by compute_file_sizes() at build time, carried through from_git's re-root, and recomputed in load_from_disk when the source directory is still present. Correct and well-scoped.
crates/csp/src/mcp.rs Wires stats_file: Option<&Path> into search_tool and find_related_tool, calling save_search_stats after results are obtained. Tests pass None to avoid touching the real stats file.
crates/csp/src/bin/csp/mcp_server.rs Adds stats_file: Option<PathBuf> to CspMcpServer, defaulting to Some(default_stats_file()) in new(). The search_tool_call_returns_json_payload test correctly nullifies the field, but find_related_tool_call_reports_missing_chunk omits the nullification (harmless here because that path returns before recording stats, but a pattern to be aware of).
crates/csp/src/bin/csp/main.rs Splits dispatch into dispatch + dispatch_with_stats(stats_file) for testability; wires telemetry into search_output and find_related_output. The Command::Savings arm inside dispatch_with_stats hard-codes default_stats_file() rather than using the injected stats_file, breaking the injection contract for that subcommand.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant CLI as csp CLI / MCP Tool
    participant dispatch as dispatch_with_stats(stats_file)
    participant search as search_output / find_related_output
    participant index as CspIndex
    participant stats as save_search_stats
    participant file as savings.jsonl

    User->>CLI: csp search "query"
    CLI->>dispatch: "dispatch_with_stats(Search{...}, stats_file)"
    dispatch->>index: load_index() → CspIndex (with file_sizes)
    index-->>dispatch: "CspIndex {chunks, file_sizes}"
    dispatch->>search: "search_output(&idx, query, top_k, max_lines, Some(stats_file))"
    search->>index: index.search(query, options)
    index-->>search: "Vec<SearchResult>"
    search->>stats: save_search_stats(stats_file, results, Search, file_sizes, max_lines)
    stats->>stats: compute snippet_chars via delivered_chars()
    stats->>stats: dedup file paths → sum file_chars
    stats->>file: append JSONL record
    search-->>dispatch: JSON string
    dispatch-->>User: stdout

    Note over index: file_sizes populated at from_path() / from_git() / load_from_disk()
    Note over stats: Best-effort: I/O errors are swallowed
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant CLI as csp CLI / MCP Tool
    participant dispatch as dispatch_with_stats(stats_file)
    participant search as search_output / find_related_output
    participant index as CspIndex
    participant stats as save_search_stats
    participant file as savings.jsonl

    User->>CLI: csp search "query"
    CLI->>dispatch: "dispatch_with_stats(Search{...}, stats_file)"
    dispatch->>index: load_index() → CspIndex (with file_sizes)
    index-->>dispatch: "CspIndex {chunks, file_sizes}"
    dispatch->>search: "search_output(&idx, query, top_k, max_lines, Some(stats_file))"
    search->>index: index.search(query, options)
    index-->>search: "Vec<SearchResult>"
    search->>stats: save_search_stats(stats_file, results, Search, file_sizes, max_lines)
    stats->>stats: compute snippet_chars via delivered_chars()
    stats->>stats: dedup file paths → sum file_chars
    stats->>file: append JSONL record
    search-->>dispatch: JSON string
    dispatch-->>User: stdout

    Note over index: file_sizes populated at from_path() / from_git() / load_from_disk()
    Note over stats: Best-effort: I/O errors are swallowed
Loading

Comments Outside Diff (2)

  1. crates/csp/src/bin/csp/main.rs, line 404-408 (link)

    P2 The Savings arm hard-codes default_stats_file() instead of using the injected stats_file. Every other subcommand in dispatch_with_stats uses stats_file for telemetry I/O, so this one diverges from the established contract. A test that redirects writes to a temp file and then calls Savings to inspect them would silently read from ~/.csp/savings.jsonl instead of the temp file.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/csp/src/bin/csp/main.rs
    Line: 404-408
    
    Comment:
    The `Savings` arm hard-codes `default_stats_file()` instead of using the injected `stats_file`. Every other subcommand in `dispatch_with_stats` uses `stats_file` for telemetry I/O, so this one diverges from the established contract. A test that redirects writes to a temp file and then calls `Savings` to inspect them would silently read from `~/.csp/savings.jsonl` instead of the temp file.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Claude Code

  2. crates/csp/src/bin/csp/mcp_server.rs, line 272-297 (link)

    P2 find_related test omits stats_file = None

    This test creates CspMcpServer::new(...), which now initialises stats_file to Some(default_stats_file()). It escapes touching ~/.csp only because the lookup for nope.ts returns before the save_search_stats call is ever reached. Any future test that exercises a successful find_related call on a bare CspMcpServer::new(...) would silently append to the developer's real savings file. Adding server.stats_file = None; here makes the guard explicit and immune to future refactors of the early-return path.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: crates/csp/src/bin/csp/mcp_server.rs
    Line: 272-297
    
    Comment:
    **`find_related` test omits `stats_file = None`**
    
    This test creates `CspMcpServer::new(...)`, which now initialises `stats_file` to `Some(default_stats_file())`. It escapes touching `~/.csp` only because the lookup for `nope.ts` returns before the `save_search_stats` call is ever reached. Any future test that exercises a *successful* `find_related` call on a bare `CspMcpServer::new(...)` would silently append to the developer's real savings file. Adding `server.stats_file = None;` here makes the guard explicit and immune to future refactors of the early-return path.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/csp/src/bin/csp/main.rs:404-408
The `Savings` arm hard-codes `default_stats_file()` instead of using the injected `stats_file`. Every other subcommand in `dispatch_with_stats` uses `stats_file` for telemetry I/O, so this one diverges from the established contract. A test that redirects writes to a temp file and then calls `Savings` to inspect them would silently read from `~/.csp/savings.jsonl` instead of the temp file.

```suggestion
        Command::Savings { verbose } => {
            print!(
                "{}",
                format_savings_report(stats_file, verbose, now_secs())
            );
```

### Issue 2 of 2
crates/csp/src/bin/csp/mcp_server.rs:272-297
**`find_related` test omits `stats_file = None`**

This test creates `CspMcpServer::new(...)`, which now initialises `stats_file` to `Some(default_stats_file())`. It escapes touching `~/.csp` only because the lookup for `nope.ts` returns before the `save_search_stats` call is ever reached. Any future test that exercises a *successful* `find_related` call on a bare `CspMcpServer::new(...)` would silently append to the developer's real savings file. Adding `server.stats_file = None;` here makes the guard explicit and immune to future refactors of the early-return path.

Reviews (1): Last reviewed commit: "feat(stats): wire token-savings telemetr..." | Re-trigger Greptile

@codspeed-hq

codspeed-hq Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 5 untouched benchmarks


Comparing amondnet/savings-wiring (c6652d0) with main (b4d7bc3)

Open in CodSpeed

Base automatically changed from amondnet/max-snippet-lines to main September 4, 2026 07:43
@amondnet

amondnet commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the max_snippet_lines option to limit the number of source code lines returned per search or find-related result, defaulting to 10 lines to optimize token usage. It also adds token-savings telemetry to track and record the characters delivered to the caller. The review feedback correctly identifies potential integer truncation risks when casting i64 to usize in the resolve_snippet_lines helper functions across both main.rs and mcp_server.rs, suggesting a safer conversion using try_from.

Comment thread crates/csp/src/bin/csp/main.rs Outdated
Comment thread crates/csp/src/bin/csp/mcp_server.rs Outdated
`save_search_stats` was defined and tested but never called — no code path
recorded to `~/.csp/savings.jsonl`, so `csp savings` always reported zero.
Wire it in at the CLI/MCP app boundary (keeping `CspIndex::search` a pure,
side-effect-free library call) and apply the semble#206 snippet accounting.

- CspIndex gains `file_sizes` (repo-relative path → UTF-16 char count),
  captured at build time in `from_path`, carried through `from_git`'s
  re-root, and recomputed in `load_from_disk` when the source root is a
  still-present local directory (mirrors semble reading sizes off `root`).
- `save_search_stats` takes `max_snippet_lines` so recorded `snippet_chars`
  reflects what the caller actually received (semble#206): `None` → full
  chunk, `Some(0)` → 0, `Some(n)` → first n lines.
- Recording happens in `search_output` / `find_related_output` (CLI) and
  `search_tool` / `find_related_tool` (MCP), each behind an injected
  `Option<&Path>` stats file so tests redirect telemetry off the real
  `~/.csp/savings.jsonl`. The MCP server and CLI dispatch default to the
  real file; tests pass a temp file or `None`.

Closes #81.
- guard compute_file_sizes against absolute or parent-escaping chunk paths
  from a tampered on-disk index
- compute delivered_chars without materializing the joined snippet
@amondnet
amondnet force-pushed the amondnet/savings-wiring branch from 8318050 to c94908a Compare September 4, 2026 09:24
@amondnet

amondnet commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces token-savings telemetry to the search and find-related operations by tracking file sizes and recording search statistics. The review feedback highlights a high-severity security risk in compute_file_sizes where symlinks or special files are not validated before reading, which could lead to a Denial of Service. Additionally, storing relative root paths in CspIndex::from_path may cause incorrect file size computations when loaded from different directories, and the search_tool function violates the style guide by exceeding the limit of five parameters.

Comment thread crates/csp/src/indexing/index.rs Outdated
Comment thread crates/csp/src/indexing/index.rs Outdated
Comment thread crates/csp/src/mcp.rs
- store from_path root as an absolute path (upstream path.resolve() parity)
- read only regular files in compute_file_sizes (skip symlinks, dirs, FIFOs)
- document the too_many_arguments allow on search_tool like find_related_tool
@amondnet

amondnet commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

/gemini review

@sonarqubecloud

sonarqubecloud Bot commented Sep 4, 2026

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces token-savings telemetry to track and record character savings during search and find-related operations. It adds a file_sizes map to CspIndex to store per-file character counts, computes these sizes during index creation or load, and records telemetry via save_search_stats while respecting snippet line limits. Feedback on the changes highlights a performance concern where eagerly computing file sizes for all chunks during index load introduces significant disk I/O overhead, and suggests making this computation lazy.

Comment thread crates/csp/src/indexing/index.rs
@amondnet
amondnet merged commit 8b7dd44 into main Sep 4, 2026
13 checks passed
@amondnet
amondnet deleted the amondnet/savings-wiring branch September 4, 2026 09:40
amondnet added a commit that referenced this pull request Sep 4, 2026
amondnet added a commit that referenced this pull request Sep 4, 2026
…vectors, and BM25 postings (#91)

* feat(index): incremental reindexing — reuse unchanged files' chunks, vectors, and BM25 postings

Port upstream semble #225 (partial reindexing) to the Rust core. When the
cached index's whole-tree content hash is stale, `load_or_build_index` now
seeds the rebuild with the previous index instead of rebuilding from
scratch: files whose per-file content hash is unchanged keep their chunks,
vector rows, and BM25 postings; only changed files are re-chunked and
re-embedded, and deleted files' postings are dropped.

- `indexing/types.rs`: `FileManifestEntry {hash, start, count}`,
  `PreviousIndex::try_new` (alignment checks), `make_chunk_id`.
- `sparse.rs`: `Bm25Index` becomes the id-keyed incremental index from
  upstream `bm25.py` (`add_document` / `remove_document` /
  `set_doc_order`); `bm25.json` v2 persists `{documents, docOrder}`.
- `create.rs`: `create_index_from_path(.., previous)` reuse path; rows
  are moved (not copied) and reused rows are not re-normalised.
- `cache_orchestrator.rs`: `load_previous_for_incremental` (fails closed
  on any structural inconsistency) + shared `manifest_compatible`.
- `index.rs`: `files` manifest in `IndexManifest`/`CspIndex`,
  `from_path_with_previous`, `INDEX_SCHEMA_VERSION` 1 → 2, and
  `load_from_disk` rejects component count mismatches.
- ADR-0005 records the per-file content hash (vs upstream `mtime_ns`)
  decision; `semble.md` and both READMEs updated.

Refs #84

* fix(index): harden incremental reindex after review

- `PreviousIndex::try_new`: sort manifest entries by `(start, count)` so a
  zero-chunk file that ties with the following file no longer fails the
  tiling check (which silently disabled incremental reuse for that tree).
  Regression test `zero_chunk_file_does_not_break_manifest_tiling`.
- `Bm25Index::load`: rebuild postings from the persisted term counts via
  `insert_document` instead of materialising `freq` copies of every term;
  sum lengths in u64 and reject out-of-range counts. Drop the duplicate
  `Doc.chunk_id`.
- `create_index_from_path`: embed all changed files' chunks in one batched
  pass (`dense::embed_chunk_refs`) so a cold build keeps the tokenizer's
  batch parallelism.
- `FileManifestEntry::end()`: saturating add so a corrupt manifest fails
  the range checks instead of overflowing.
- `load_previous_for_incremental`: reject a seed whose vector rows do not
  match the live model's dimension, falling back to a full rebuild.
- `parse_manifest`: read `files` through the `FileManifestEntry` serde
  derive that `save` writes with.
- Docs: query-term de-duplication is a real ranking divergence from
  upstream's query-frequency weighting, not rank-neutral; record it as an
  open parity gap in ADR-0005 and `semble.md`.

Refs #84

* fix(index): skip files whose lossy display path collides with an indexed file

On Unix, file names that differ only in invalid UTF-8 bytes collapse to
the same `to_string_lossy` path. The BM25 chunk ids derived from that
path would then collide and abort the whole build with
"chunk_id already indexed". Keep the first such file, skip the rest
with a warning, and add a Linux-only regression test (APFS rejects
non-UTF-8 names).

Refs #84

* chore: merge origin/main (#80 max_snippet_lines, #82 savings telemetry) into incremental reindexing

* fix(index): reject zero BM25 term counts on load; take persisted vectors verbatim

- Bm25Index::load rejects a zero term frequency (it would inflate the
  term's document frequency) so the cache falls back to a full rebuild.
- SelectableBasicBackend::load no longer re-normalises rows that were
  normalised before save, keeping unchanged rows bit-identical across an
  incremental rebuild seeded from disk.

Refs #84

* refactor(index): split create/sparse tests out, extract create_index_from_path helpers

- create.rs / sparse.rs test modules move to create/tests.rs and
  sparse/tests.rs, matching the index/, dense/, cache_orchestrator/ layout.
- create_index_from_path delegates to open_previous, display_path,
  take_previous_rows and embed_fresh_rows; behaviour unchanged.
- load_previous_for_incremental compares the content selection as a set,
  so a duplicated request no longer matches a manifest that covers more.

Refs #84

* perf(index): compare the cached backend dim instead of scanning every row

Refs #84

* test(index): build the manifest key with the platform separator

Refs #84
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

savings telemetry is not wired into the search flow (blocks semble#206)

1 participant